Android 多返回栈技术详解
Navigation 2.4.0-alpha01
https://developer.android.google.cn/jetpack/androidx/releases/navigation#2.4.0-alpha01Fragment 1.4.0-alpha01
https://developer.android.google.cn/jetpack/androidx/releases/fragment#1.4.0-alpha01
系统返回按钮的乐趣
OnBackPressedDispatcher
https://developer.android.google.cn/reference/androidx/activity/OnBackPressedDispatcher针对自定义返回导航的 API
https://developer.android.google.cn/guide/navigation/navigation-custom-backFragmentManager
https://developer.android.google.cn/reference/androidx/fragment/app/FragmentManagerNavController
https://developer.android.google.cn/reference/kotlin/androidx/navigation/NavController
Fragment 中的多返回栈
在 surface 层级,对于多返回栈的支持貌似很直接,但其实需要额外解释一下 "Fragment 返回栈" 到底是什么。FragmentManager 的返回栈其实包含的不是 Fragment,而是由 Fragment 事务组成的。更准确地说,是由那些调用了 addToBackStack(String name) API 的事务组成的。
多返回栈的支持
https://developer.android.google.cn/guide/fragments/fragmentmanager#multiple-back-stacksaddToBackStack(String name)
https://developer.android.google.cn/reference/androidx/fragment/app/FragmentTransaction#addToBackStack(java.lang.String)
这就意味着当您调用 commit() 提交了一个调用过 addToBackStack() 方法的 Fragment 事务时,FragmentManager 会执行所有您在事务中所指定的操作 (比如替换操作),从而将每个 Fragment 转换为预期的状态。然后 FragmentManager 会将该事务作为它返回栈的一部分。
当您调用 popBackStack() 方法时 (无论是直接调用,还是通过系统返回键以 FragmentManager 内部机制调用),Fragment 返回栈的最上层事务会从栈中弹出 -- 比如新添加的 Fragment 会被移除,隐藏的 Fragment 会显示。这会使得 FragmentManager 恢复到最初提交 Fragment 事务之前的状态。
作者注: 这里有一个非常重要的事情需要大家注意,在同一个 FragmentManager 中绝对不应该将含有 addToBackStack() 的事务和不含的事务混在一起: 返回栈的事务无法察觉返回栈之外的 Fragment 事务的修改 —— 当您从堆栈弹出一个非常不确定的元素时,这些事务从下层替换出来的时候会撤销之前未添加到返回栈的修改。
排除 Fragment 在技术上的障碍
虽然 Fragment 总是会保存 Fragment 的视图状态,但是 Fragment 的 onSaveInstanceState() 方法只有在 Activity 的 onSaveInstanceState() 被调用时才会被调用。为了能够保证调用 saveBackStack() 时 SavedInstanceState 会被保存,我们还需要在 Fragment 生命周期切换的正确时机注入对 onSaveInstanceState() 的调用。我们不能调用得太早 (您的 Fragment 不应该在 STARTED 状态下保存状态),也不能调用得太晚 (您需要在 Fragment 被销毁之前保存状态)。
Fragment 的视图状态
https://developer.android.google.cn/guide/fragments/saving-state#viewFragment 生命周期切换
https://developer.android.google.cn/guide/fragments/lifecycle#states
这样的前提条件就开启了需要解决 FragmentManager 转换到对应状态的问题,以此来保障有一个地方能够将 Fragment 转换为所需状态,并且处理可重入行为和 Fragment 内部的状态转换。
解决 FragmentManager 转换到对应状态的问题
https://issuetracker.google.com/139536619
在 Fragment 的重构工作进行了 6 个月,进行了 35 次修改时,发现 Postponed Fragment 功能已经严重损坏,这一问题使得被推迟的事务处于一个中间状态 —— 既没有被提交也并不是未被提交。之后的 65 个修改和 5 个月的时间里,我们几乎重写了 FragmentManager 管理状态、延迟状态切换和动画的内部代码,具体请参见我们之前的文章《全新的 Fragment: 使用新的状态管理器》。
Postponed Fragment 功能已经严重损坏
https://issuetracker.google.com/147749580
Fragment 中值得期待的地方
随着技术问题的逐步解决,包括更加可靠和更易理解的 FragmentManager,我们新增加了两个 API: saveBackStack() 和 restoreBackStack()。
如果您不使用这些新增 API,则一切照旧: 单个 FragmentManager 返回栈和之前的功能相同。现有的 addToBackStack() 保持不变 —— 您可以将 name 赋值为 null 或者任意 name。然而,当您使用多返回栈时,name 的作用就非常重要了: 在您调用 saveBackStack() 和之后的 restoreBackStack() 方法时,它将作为 Fragment 事务的唯一的 key。
举个例子,会更容易理解。比如您已经添加了一个初始的 Fragment 到 Activity,然后提交了两个事务,每个事务中包含一个单独的 replace 操作:
// 这是用户看到的初始的 Fragment
fragmentManager.commit {
setReorderingAllowed(true)
replace<HomeFragment>(R.id.fragment_container)
}
// 然后,响应用户操作,我们在返回栈中增加了两个事务
fragmentManager.commit {
setReorderingAllowed(true)
replace<ProfileFragment>(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {
setReorderingAllowed(true)
replace<EditProfileFragment>(R.id.fragment_container)
addToBackStack(“edit_profile”)
}
也就是说我们的 FragmentManager 会变成这样:
△ 提交三次之后的 FragmentManager 的状态
比如说我们希望将 profile 页换出返回栈,然后切换到通知 Fragment。这就需要调用 saveBackStack() 并且紧跟一个新的事务:
fragmentManager.saveBackStack("profile")
fragmentManager.commit {
setReorderingAllowed(true)
replace<NotificationsFragment>(R.id.fragment_container)
addToBackStack("notifications")
}
现在我们添加 ProfileFragment 的事务和添加 EditProfileFragment 的事务都保存在 "profile" 关键字下。这些 Fragment 已经完全将状态保存,并且 FragmentManager 会随同事务状态一起保持它们的状态。很重要的一点: 这些 Fragment 的实例并不在内存中或者在 FragmentManager 中 —— 存在的仅仅只有状态 (以及任何以 ViewModel 实例形式存在的非配置状态)。
△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态
替换回来非常简单: 我们可以在 "notifications" 事务中同样调用 saveBackStack() 操作,然后调用 restoreBackStack():
fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)
这两个堆栈项高效地交换了位置:
△ 交换堆栈项后的 FragmentManager 状态
维持一个单独且活跃的返回栈并且将事务在其中交换,这保证了当返回按钮被点击时,FragmentManager 和系统的其他部分可以保持一致的响应。实际上,整个逻辑并未改变,同之前一样,仍然弹出 Fragment 返回栈的最后一个事务。
这些 API 都特意按照最小化设计,尽管它们会产生潜在的影响。这使得开发者可以基于这些接口设计自己的结构,而无需通过任何非常规的方式保存 Fragment 的视图状态、已保存的实例状态、非配置的状态。
当然了,如果您不希望在这些 API 之上构建您的框架,那么可以使用我们所提供的框架进行开发。
使用 Navigation 将多返回栈适配到任意屏幕类型
Navigation Component
https://developer.android.google.cn/guide/navigation/NavHost 接口
https://developer.android.google.cn/reference/kotlin/androidx/navigation/NavHostNavigator
https://developer.android.google.cn/reference/kotlin/androidx/navigation/Navigator
该级别的分离意味着 Navigation 中有两个层次来实现多返回栈:
保存独立的 NavBackStackEntry 实例状态,这些实例组成了 NavController 返回栈。这是属于 NavController 的职责。
保存 Navigator 针对每个 NavBackStackEntry 的特定状态 (比如与 FragmentNavigator 目的地相关联的 Fragment)。这是属于 Navigator 的职责。
NavBackStackEntry
https://developer.android.google.cn/reference/kotlin/androidx/navigation/NavBackStackEntry
备注: 通过绑定 TestNavigatorState 使其成为一个 mini-NavController 可以实现在新的 Navigator API 上更轻松、独立地测试您自定义的 Navigator。
TestNavigatorState
https://developer.android.google.cn/reference/kotlin/androidx/navigation/testing/TestNavigatorState
在 Navigation 中启用多返回栈
NavigationUI
https://developer.android.google.cn/guide/navigation/navigation-ui
onClick = {
navController.navigate(screen.route) {
// 当用户选择子项时在返回栈中弹出到导航图中的起始目的地
// 来避免太过臃肿的目的地堆栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// 当重复选择相同项时避免相同目的地的多重拷贝
launchSingleTop = true
// 当重复选择之前已经选择的项时恢复状态
restoreState = true
}
}
与底部导航栏集成
https://developer.android.google.cn/jetpack/compose/navigation#bottom-nav
保存状态,锁定用户
Fragments (比如完全不使用 Navigation Component): 通过使用新的 FragmentManager API,也就是 saveBackStack 和 restoreBackStack。 核心的 Navigation 运行时: 添加可选的新的 NavOptions 方法用于 restoreState (恢复状态) 和 saveState (保存状态) 以及新的 popBackStack() 的重载方法,它同样可以传入一个布尔型的 saveState 参数 (默认是 false)。 通过 Fragment 实现 Navigation: FragmentNavigator 现在利用新的 Navigator API,通过使用 Navigation 运行时 API 将 Navigation 运行时 API 转换为 Fragment API。 NavigationUI: 每当它们弹出返回栈时,onNavDestinationSelected()、NavigationBarView.setupWithNavController() 和 NavigationView.setupWithNavController() 现在默认使用 restoreState 和 saveState 这两个新的 NavOption。也就意味着当升级到 Navigation 2.4.0-alpha01 或者更高版本后,任何使用 NavigationUI API 的应用无需修改代码即可实现多返回栈。
保存与 Fragment 相关的状态
https://developer.android.google.cn/guide/fragments/saving-state
如果您希望了解更多使用该 API 的示例,请参考 NavigationAdvancedSample (它是最新更新的,且不包含任何用于支持多返回栈的 NavigationExtensions 代码):
如果您遇到任何问题,请使用官方的问题追踪页面提交关于 Fragment 或者 Navigation 的 bug,我们会尽快处理。
Fragment
https://issuetracker.google.com/issues/new?component=460964Navigation
https://issuetracker.google.com/issues/new?component=409828
推荐阅读